/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import fr.x9c.cadmium.util.IO;
import fr.x9c.cadmium.util.Misc;

/**
 * This class allows writting of values.
 *
 * @see java.io.DataInput
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.3
 * @since 1.0
 */
public final class Extern implements DataFormat {

    /** Integer value of 'No_sharing' flag. */
    private static final int NO_SHARING = 1;

    /** Integer value of 'Closures' flag. */
    private static final int CLOSURES = 2;

    /** Description of flags for flag list conversion. */
    private static final int[] EXTERN_FLAGS = {
        Extern.NO_SHARING,
        Extern.CLOSURES
    };

    /**
     * No instance of this class.
     */
    private Extern() {
    } // end empty constructor

    /**
     * Converts a value into an array of bytes.
     * @param ctxt context - should not be <tt>null</tt>
     * @param v value to convert - should not be <tt>null</tt>
     * @param flags flag list controlling conversion
     *              - should not be <tt>null</tt>
     * @return <tt>v</tt> as a byte array
     * @throws IOException if an I/O error occurs
     * @throws Fail.Exception if value is not serializable
     */
     public static byte[] externValue(final Context ctxt,
                                      final Value v,
                                      final Value flags)
        throws IOException, Fail.Exception {
        assert ctxt != null : "null ctxt";
        assert v != null : "null v";
        assert flags != null : "null flags";

        final int fl = fr.x9c.cadmium.kernel.Misc.convertFlagList(flags, Extern.EXTERN_FLAGS);
        final boolean ignoreSharing = (fl & Extern.NO_SHARING) != 0;
        final boolean closures = (fl & Extern.CLOSURES) != 0;
        final List<Value> objTable = new LinkedList<Value>();
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final Custom.SerializationSizes sizes =
            new Custom.SerializationSizes();
        final DataOutput out = new DataOutputStream(baos);

        IO.write32s(out, MAGIC_NUMBER);
        out.write(new byte[16]); // empty space for sizes
        externRec(ctxt, out, v, objTable, sizes, ignoreSharing, closures);
        final byte[] res = baos.toByteArray();
        baos.close();

        final ByteArrayOutputStream tmp = new ByteArrayOutputStream(16);
        final DataOutput tmpOut = new DataOutputStream(tmp);
        tmpOut.writeInt(res.length - 20);
        tmpOut.writeInt(objTable.size());
        tmpOut.writeInt(Misc.unsignedToSigned(sizes.size32));
        tmpOut.writeInt(Misc.unsignedToSigned(sizes.size64));
        System.arraycopy(tmp.toByteArray(), 0, res, 4, 16);
        tmp.close();

        return res;
    } // end method 'externValue(Context, Value, Value)'

    /**
     * Serializes a value.
     * @param ctxt context - should not be <tt>null</tt>
     * @param out where to serialize value - should not be <tt>null</tt>
     * @param v value to serialize - should not be <tt>null</tt>
     * @param objTable list of already seen objects (initially empty)
     *                 - should not be <tt>null</tt>
     * @param sizes size of value in bytes - should not be <tt>null</tt>
     * @param ignoreSharing whether block sharing should be ignored
     * @param closures whether closures may be serialized
     * @throws IOException if an I/O error occurs
     * @throws Fail.Exception if value is not serializable
     */
    private static void externRec(final Context ctxt,
                                  final DataOutput out,
                                  final Value v,
                                  final List<Value> objTable,
                                  final Custom.SerializationSizes sizes,
                                  final boolean ignoreSharing,
                                  final boolean closures)
        throws IOException, Fail.Exception {
        assert ctxt != null : "null ctxt";
        assert out != null : "null out";
        assert v != null : "null v";
        assert objTable != null : "null objTable";
        assert sizes != null : "null sizes";

        if (v.isLong()) {
            final int n = v.asLong();
            if ((n >= 0) && (n < 0x40)) {
                IO.write8u(out, n + PREFIX_SMALL_INT);
            } else if ((n >= Byte.MIN_VALUE) && (n <= Byte.MAX_VALUE)) {
                IO.write8u(out, CODE_INT8);
                IO.write8s(out, n);
            } else if ((n >= Short.MIN_VALUE) && (n <= Short.MAX_VALUE)) {
                IO.write8u(out, CODE_INT16);
                IO.write16s(out, n);
            } else {
                IO.write8u(out, CODE_INT32);
                IO.write32s(out, n);
            } // end if/elsif/else
            return;
        } // end if

        if (v.isBlock()) {
            final Block bl = v.asBlock();
            final int hd = bl.getHeader();
            final int tag = bl.getTag();
            final int sz = bl.getWoSize();
            int h;

            if (tag == Block.FORWARD_TAG) {
                final Value f = bl.get(0);
                if (f.isBlock() && !ctxt.isAtom(f)
                    && ((f.asBlock().getTag() == Block.FORWARD_TAG)
                        || (f.asBlock().getTag() == Block.LAZY_TAG)
                        || (f.asBlock().getTag() == Block.DOUBLE_TAG))) {
                    // treated later
                } else {
                    externRec(ctxt,
                              out,
                              f,
                              objTable,
                              sizes,
                              ignoreSharing,
                              closures);
                    return;
                } // end if/else
            } // end if

            if (sz == 0) { // atoms
                if (tag < 16) {
                    IO.write8u(out, PREFIX_SMALL_BLOCK + tag);
                } else {
                    IO.write8u(out, CODE_BLOCK32);
                    IO.write32s(out, hd);
                } // end if/else
                return;
            } // end if

            // already seen objects
            if (!ignoreSharing && (tag != Block.INFIX_TAG)) {
                final int idx = objTable.indexOf(v);
                if (idx != -1) {
                    final int ofs = objTable.size() - idx;
                    if (ofs < 0x100) {
                        IO.write8u(out, CODE_SHARED8);
                        IO.write8u(out, ofs);
                    } else if (ofs < 0x10000) {
                        IO.write8u(out, CODE_SHARED16);
                        IO.write16u(out, ofs);
                    } else {
                        IO.write8u(out, CODE_SHARED32);
                        IO.write32s(out, ofs);
                    } // end if/elsif/else
                    return;
                } // end if
                objTable.add(v);
            } // end if

            switch (tag) {
            case Block.STRING_TAG:
                final int len = bl.sizeBytes();
                if (len < 0x20) {
                    IO.write8u(out, PREFIX_SMALL_STRING + len);
                } else if (len < 0x100) {
                    IO.write8u(out, CODE_STRING8);
                    IO.write8u(out, len);
                } else {
                    IO.write8u(out, CODE_STRING32);
                    IO.write32u(out, len);
                } // end if/elsif/else
                out.write(bl.getBytes());
                sizes.size32 += 1 + (len + 4) / 4;
                sizes.size64 += 1 + (len + 8) / 8;
                break;
            case Block.DOUBLE_TAG:
                IO.write8u(out, CODE_DOUBLE_NATIVE);
                if (fr.x9c.cadmium.kernel.Misc.isBigEndianPlatform()) {
                    IO.write64s(out, Double.doubleToRawLongBits(bl.asDouble()));
                } else {
                    IO.write64s(out, Long.reverseBytes(Double.doubleToRawLongBits(bl.asDouble())));
                } // end if/else
                sizes.size32 += 1 + 2;
                sizes.size64 += 1 + 1;
                break;
            case Block.DOUBLE_ARRAY_TAG:
                final int nFloats = bl.sizeDoubles();
                if (nFloats < 0x100) {
                    IO.write8u(out, CODE_DOUBLE_ARRAY8_NATIVE);
                    IO.write8u(out, nFloats);
                } else {
                    IO.write8u(out, CODE_DOUBLE_ARRAY32_NATIVE);
                    IO.write32u(out, nFloats);
                } // end if/else
                if (fr.x9c.cadmium.kernel.Misc.isBigEndianPlatform()) {
                    for (int i = 0; i < nFloats; i++) {
                        IO.write64s(out,
                                    Double.doubleToRawLongBits(bl.getDouble(i)));
                    } // end for
                } else {
                    for (int i = 0; i < nFloats; i++) {
                        IO.write64s(out,
                                    Long.reverseBytes(Double.doubleToRawLongBits(bl.getDouble(i))));
                    } // end for
                } // end if/else
                sizes.size32 += 1 + nFloats * 2;
                sizes.size64 += 1 + nFloats;
                break;
            case Block.ABSTRACT_TAG:
                Fail.invalidArgument("output_value: abstract value (Abstract)");
                break;
            case Block.INFIX_TAG:
                final Block infixBlock = v.asBlock();
                IO.write8u(out, CODE_INFIXPOINTER);
                IO.write32s(out, 4 * infixBlock.getWoSize());
                externRec(ctxt,
                          out,
                          Value.createFromBlock(infixBlock.getParent()),
                          objTable,
                          sizes,
                          ignoreSharing,
                          closures);
                break;
            case Block.CUSTOM_TAG:
                final Custom.Operations ops = bl.getCustomOperations();
                if (!ops.isSerializable()) {
                    Fail.invalidArgument("output_value: abstract value (Custom)");
                } // end if
                IO.write8u(out, CODE_CUSTOM);
                out.write(Misc.convertStringToBytes(ops.getIdentifier()));
                IO.write8u(out, 0x00);
                final Custom.SerializationSizes tmpSizes =
                    ops.serialize(out, v);
                sizes.size32 += 2 + ((tmpSizes.size32 + 3) >> 2);
                sizes.size64 += 2 + ((tmpSizes.size64 + 7) >> 3);
                break;
            default:
                if ((tag < 16) && (sz < 8)) {
                    IO.write8u(out, PREFIX_SMALL_BLOCK + tag + (sz << 4));
                } else {
                    IO.write8u(out, CODE_BLOCK32);
                    IO.write32s(out, hd);
                } // end if/else
                sizes.size32 += 1 + sz;
                sizes.size64 += 1 + sz;
                for (int i = 0; i < sz; i++) {
                    externRec(ctxt,
                              out,
                              bl.get(i),
                              objTable,
                              sizes,
                              ignoreSharing,
                              closures);
                } // end for
                break;
            } // end switch
            return;
        } // end if

        if (v.isCodeOffset()) {
            final int ofs = v.asCodeOffset();
            if (!closures || (ofs >= ctxt.getOriginalCodeSize())) {
                Fail.invalidArgument("output_value: functional value");
            } // end if
            IO.write8u(out, CODE_CODEPOINTER);
            IO.write32s(out, ofs);
            out.write(ctxt.getCodeDigest());
            return;
        } // end if

        Fail.invalidArgument("output_value: abstract value (outside heap)");
    } // end method 'externRec(Context, DataOutput, Value, List<Value>, ...)

} // end class 'Extern'
